package com.pmstation.skin

import java.awt.image.BufferedImage
import annotation.tailrec
import collection.mutable
import java.awt.Color
import java.io.File
import javax.imageio.ImageIO

/**
Main guideline:
An algorighm for nudity detection
 http://www.math.admu.edu.ph/~raf/pcsc05/proceedings/AI4.pdf
 */

case class NudityResult(nude:Boolean, testId:Char, cause:String)

class NudityDetection(img:BufferedImage, isSkinPixel:Int=>Boolean ){
  case class Stripe(line:Int, from:Int, to:Int)

  val w = img.getWidth
  val h = img.getHeight
  val skin = Array.ofDim[Boolean](w,h)

  for( i<-0 until img.getWidth; j<-0 until img.getHeight ){
    val skinij = isSkinPixel( img.getRGB(i,j) )
    skin(i)(j) = skinij
  }
  val countSkin = skin.flatten.count( (b:Boolean) => b )

  def check() : NudityResult = {
    if( countSkin < 0.15 * w * h ){
      NudityResult(false,'a',"the percentage of skin pixels relative to the image size is less than 15 percent")
    }else{
      val regions = new Regions

      if( regions.sizeBig1st < 0.35 * countSkin  && regions.sizeBig2nd < 0.30 * countSkin
          && regions.sizeBig3rd < 0.30 * countSkin ){
      NudityResult(false,'b',
"""the number of skin pixels in the largest skin region is less than 35% of the total
 skin count, the number of skin pixels in the second largest region is less than
30% of the total skin count and the number of skin pixels in the third largest
region is less than 30 % of the total skin count""")

      }else if( regions.sizeBig1st + regions.sizeBig2nd + regions.sizeBig3rd < 0.45 * countSkin ){
        //NudityResult(false,'c',"the number of skin pixels in the largest skin region is less than 45% of the total skin count")
        // mistprint??? rule changed to:
        NudityResult(false,'c',"the number of skin pixels in the 3 largest skin regions is less than 45% of the total skin count")
      }else{
        val poly = new regions.BoundingPolygon
        if( countSkin < 0.30 * w * h && poly.areaSkin<0.55*poly.area ){
          NudityResult(false,'d',
"""the total skin count is less than 30% of the total number of pixels in the image
and the number of skin pixels within the bounding polygon is less than 55 percent
of the size of the polygon""" )

        }else if( regions.regSizesSorted.size>60 && poly.sumIntensity < 0.25 * poly.area * 255 * 3 ){
          NudityResult(false,'e',
"the number of skin regions is more than 60 and the average intensity within the polygon is less than 0.25")
        }else
          NudityResult(true,'f',"")
      }
    }
  }

  def isSkin(line:Int, x:Int) = {
    if( line<0 || line>=h || x<0 || x>=w )
      false
    else
      skin(x)(line)
  }


  case class RegWithSize(regId:Int, size:Int)

  class Regions{
    val regions = Array.ofDim[Int](w,h)

    findRegions()

    val regSizes = new mutable.HashMap[Int,Int]
    for( i<-0 until img.getWidth; j<-0 until img.getHeight ){
      val r = regions(i)(j)
      if( r>0 ){
//      val near = Set( getRegion(j,i+1), getRegion(j,i-1), getRegion(j-1,i), getRegion(j+1,i) )
//      if( !(near - 0 - r).isEmpty ){
//        println("regions touching!!! %s  r=%s line=%s x=%s".format(near,r,j,i) )
//      }
        regSizes.update(r, regSizes.getOrElse(r,0)+1)
      }
    }


    //printRegions()

    val regSizesSorted = regSizes.toList.map( e => RegWithSize(e._1, e._2) ).sortBy(-_.size)
    println("regsizes %s ".format(regSizesSorted))

    val largestPerc = regSizesSorted.head.size / (h*w)

    val regIdsLargest = regSizesSorted.map(_.regId).take(3)

    val sizeBig1st = if( regSizesSorted.size>0 ) regSizesSorted(0).size else 0
    val sizeBig2nd = if( regSizesSorted.size>1 ) regSizesSorted(1).size else 0
    val sizeBig3rd = if( regSizesSorted.size>2 ) regSizesSorted(2).size else 0

    // http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/
    case class Arrow(from:XY, to:XY){
      def leftRight(p:XY) = (p.y - from.y) * (to.x - from.x) - (p.x - from.x) * (to.y - from.y)
    }
    
    class BoundingPolygon{
      var left, right, top, bottom = null : XY
      for( i<-0 until img.getWidth; j<-0 until img.getHeight ){
        val r = regions(i)(j)
        if( regIdsLargest contains r ){
          val xy = XY(i,j)
          if(left==null){
            left = xy
            right = xy
            top = xy
            bottom = xy
          }else{
            left = left.xLeast(xy)
            right = right.xMost(xy)
            top = top.yLeast(xy)
            bottom = bottom.yMost(xy)
          }
        }
      }

      val arrows = Array( Arrow(left,top), Arrow(top,right), Arrow(right,bottom), Arrow(bottom,left) )

      var area, areaSkin=0
      var sumIntensity : Long = 0
      for( i<-left.x to right.x; j<-top.y to bottom.y ){
        if( inside(XY(i,j)) ){
          area += 1
          if( isSkin(j,i) ){
            areaSkin += 1
          }
          val c = new Color(img.getRGB(i,j))
          sumIntensity += (c.getRed + c.getBlue + c.getGreen)
        }
      }

      def inside(p:XY):Boolean = {
//        val posa = arrows.map(_.leftRight(p)).toArray
//        if(math.random<0.01){
//          println("posa=%s arrows=%s".format(posa.toList, arrows.toList))
//        }

        arrows(0).leftRight(p)>=0 && arrows(1).leftRight(p)>=0 &&
        arrows(2).leftRight(p)>=0 && arrows(3).leftRight(p)>=0
      }

      def showPolygon(fname:String){
        val ireg = new BufferedImage(w,h, BufferedImage.TYPE_INT_RGB)
        ireg.getGraphics.drawImage(img, 0,0, null)

        for( i<-0 until img.getWidth; j<-0 until img.getHeight ){
          if( inside(XY(i,j)) ){
            //ireg.setRGB(i,j, new Color(ireg.getRGB(i,j)).darker.darker.getRGB )
            ireg.setRGB(i,j, Color.YELLOW.getRGB )
          }
        }
        ImageIO.write(ireg, "jpeg", new File(fname))
        println("showPolygon %s ready".format(fname))
      }

    }

    def printRegions(){
      for( line <- 0 until h){
        for(x <- 0 until w){
          printf("%4s ",getRegion(line,x))
        }
        println()
      }
    }

    def showRegions(fname:String){
      val colorsMap = regSizesSorted.map(_.regId).zipWithIndex.toMap
      //println("colorsMap "+colorsMap)

      val colors = Array(Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.ORANGE, Color.CYAN)
      val ireg = new BufferedImage(w,h, BufferedImage.TYPE_INT_RGB)
      ireg.getGraphics.drawImage(img, 0,0, null)
      for( i<-0 until img.getWidth; j<-0 until img.getHeight ){
        val r = regions(i)(j)
        if( r>0 ){
          val colorIdx = colorsMap(r)
          if( colorIdx < colors.size ){
            ireg.setRGB(i,j, colors(colorIdx).getRGB)
          }
        }
      }
      ImageIO.write(ireg, "jpeg", new File(fname))
      println("showRegions %s ready".format(fname))
    }

    def getRegion(line:Int, x:Int) = {
      if( line<0 || line>=h || x<0 || x>=w )
        0
      else
        regions(x)(line)
    }

    def findRegions(){
      var regionId=1
      for( j<-0 until img.getHeight; i<-0 until img.getWidth ){
        if( skin(i)(j) && regions(i)(j)==0 ){
          spreadRegion(regionId, List(fillStripe(regionId, j, i)) )
          regionId += 1
        }
      }
    }

    def fillStripe(regId:Int, line:Int, x:Int) = {
      var from, to = x
      regions(x)(line) = regId
      while(isSkin(line,from-1)){
        from -= 1
        regions(from)(line) = regId
      }
      while(isSkin(line,to+1)){
        to +=1
        regions(to)(line) = regId
      }
      Stripe(line, from, to)
    }

    @tailrec
    private def spreadRegion(regId:Int, scanList:List[Stripe]){
      val s = scanList.head
      val line = s.line
      var newScan = scanList.tail

      for( x <- s.from to s.to ){
        if( getRegion(line+1, x)==0 ){
          if( isSkin(line+1, x) ){
            newScan = fillStripe(regId, line+1, x) :: newScan
          }
        }
        if( getRegion(line-1, x)==0 && isSkin(line-1, x) ){
          newScan = fillStripe(regId, line-1, x) :: newScan
        }
      }

      if( !newScan.isEmpty ){
        spreadRegion(regId, newScan)
      }
    }

  }

}